package com.dbms.storage;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * DiskManager (磁盘管理器)
 * 核心职责：作为数据库存储系统的最底层组件，直接与物理磁盘上的数据库文件交互，
 * 屏蔽磁盘I/O的底层细节，为上层组件（如缓冲池管理器）提供统一的「页级」读写接口，
 * 确保数据以固定大小的“页面”为单位在磁盘与内存间传输。
 */
public class DiskManager {

    // --- 常量定义：数据库页和文件的基础配置 ---
    /** 数据库页面大小，固定为4KB（4096字节），是磁盘I/O的最小单位 */
    public static final int PAGE_SIZE = 4096;
    /** 默认数据库文件名，当未指定自定义文件名时使用 */
    private static final String DB_FILE_NAME = "my_database.db";

    // --- 成员变量：磁盘文件操作的核心资源 ---
    /**
     * 随机访问文件对象，支持对数据库文件的任意位置读写（而非顺序读写），
     * 是实现“按页访问”的关键（可直接定位到指定页面的偏移量）
     */
    private RandomAccessFile dbFile;
    /** 数据库文件当前的总页数，记录文件中已分配的页面数量（从0开始编号） */
    private int numPages;

    /**
     * 自定义文件名的构造函数（核心修改，用于测试和多数据库实例场景）
     * 作用：通过传入自定义文件名，初始化指定的数据库文件，计算当前总页数
     * @param dbFileName 自定义的数据库文件名（含路径，如"test_db.db"）
     * @throws IOException 文件创建、打开失败时抛出
     */
    public DiskManager(String dbFileName) throws IOException {
        // 1. 根据文件名创建File对象，映射磁盘上的文件
        File file = new File(dbFileName);
        // 2. 以"rwd"模式打开文件：r=可读，w=可写，d=数据更新直接写入磁盘（跳过缓存，保证持久性）
        this.dbFile = new RandomAccessFile(file, "rwd");
        // 3. 计算当前总页数：文件总长度 / 页面大小（整数除法，不足一页的部分不计入）
        this.numPages = (int) (this.dbFile.length() / PAGE_SIZE);
        // 4. 打印初始化信息，便于调试和监控
        System.out.println("数据库文件 '" + dbFileName + "' 已打开，当前总页数: " + numPages);
    }

    /**
     * 默认构造函数（使用预定义的默认文件名DB_FILE_NAME）
     * 作用：初始化默认数据库文件，适用于单数据库实例场景
     * @throws IOException 文件创建、打开失败时抛出
     */
    public DiskManager() throws IOException {
        // 1. 使用默认文件名创建File对象
        File file = new File(DB_FILE_NAME);

        // 2. 关键："rwd"模式说明
        // - r: 允许读取文件内容
        // - w: 允许写入/修改文件内容
        // - d: 强制所有内容更新（包括数据和元数据）直接写入物理磁盘，而非仅存在操作系统缓存
        // 此模式是保证数据库数据持久性的关键，避免因系统崩溃导致缓存数据丢失
        this.dbFile = new RandomAccessFile(file, "rwd");

        // 3. 计算总页数：文件长度 / 页面大小（仅统计完整的页面）
        this.numPages = (int) (this.dbFile.length() / PAGE_SIZE);
        System.out.println("数据库文件已打开，当前总页数: " + numPages);
    }

    /**
     * 从磁盘读取指定页面到内存字节数组
     * 核心逻辑：定位页面在文件中的偏移量 → 读取完整页面数据到数组
     * @param pageId 要读取的页面ID（从0开始连续编号，唯一标识一个页面）
     * @param pageData 用于接收页面数据的内存字节数组（必须预先分配PAGE_SIZE大小的空间）
     * @throws IOException 磁盘读取失败或文件操作异常时抛出
     */
    public void readPage(int pageId, byte[] pageData) throws IOException {
        // 1. 合法性检查：防止读取不存在的页面（pageId超出当前总页数）
        if (pageId >= numPages) {
            System.err.println("错误: 尝试读取不存在的页面 " + pageId);
            return;
        }
        // 2. 合法性检查：确保接收数组大小正确（必须是PAGE_SIZE，否则无法存储完整页面）
        if (pageData.length != PAGE_SIZE) {
            System.err.println("错误: 传入的字节数组大小不等于PAGE_SIZE");
            return;
        }

        // 3. 计算页面在磁盘文件中的起始偏移量
        // 公式：偏移量 = 页面ID × 页面大小（每个页面占PAGE_SIZE字节，按顺序存储）
        // 示例：pageId=1 → 偏移量=1×4096=4096（从文件第4097字节开始读取）
        long offset = (long) pageId * PAGE_SIZE;

        // 4. 移动文件读写指针到页面的起始偏移量（随机访问的核心操作）
        dbFile.seek(offset);

        // 5. 从当前指针位置读取PAGE_SIZE字节的数据到pageData数组（读取完整页面）
        dbFile.read(pageData);
    }

    /**
     * 将内存中的页面数据写入磁盘指定页面
     * 核心逻辑：定位页面偏移量 → 写入完整页面数据（若页面不存在则自动扩展文件）
     * @param pageId 要写入的页面ID（可超出当前总页数，此时会自动扩展文件）
     * @param pageData 存储要写入页面数据的字节数组（必须是PAGE_SIZE大小，保证数据完整）
     * @throws IOException 磁盘写入失败或文件操作异常时抛出
     */
    public void writePage(int pageId, byte[] pageData) throws IOException {
        // 1. 合法性检查：确保写入数据的数组大小正确（必须是PAGE_SIZE，避免数据不完整）
        if (pageData.length != PAGE_SIZE) {
            System.err.println("错误: 尝试写入的字节数组大小不等于PAGE_SIZE");
            return;
        }

        // 2. 计算页面在磁盘文件中的起始偏移量（同readPage逻辑）
        long offset = (long) pageId * PAGE_SIZE;

        // 3. 移动文件读写指针到页面的起始偏移量
        dbFile.seek(offset);

        // 4. 将pageData数组中的PAGE_SIZE字节数据写入磁盘（覆盖或创建新页面）
        // 关键：若pageId超出当前numPages，RandomAccessFile会自动扩展文件到对应大小
        dbFile.write(pageData);
    }

    /**
     * 分配一个新的页面（逻辑分配，非物理创建）
     * 核心逻辑：返回当前总页数作为新页面ID，同时递增总页数（物理创建在writePage时触发）
     * @return 新分配的页面ID（唯一且连续，从0开始）
     */
    public int allocatePage() {
        // 逻辑：新页面ID = 当前总页数（因为页面从0开始编号，当前最后一页是numPages-1）
        int newPageId = numPages;
        // 递增总页数，标记新页面已被分配（后续writePage会实际创建该页面）
        numPages++;
        return newPageId;
    }

    /**
     * 获取当前数据库文件的总页数（已分配的页面数量）
     * @return 总页数（int类型，页面ID范围：0 ~ numPages-1）
     */
    public int getNumPages() {
        return numPages;
    }

    /**
     * 关闭数据库文件，释放文件资源
     * 作用：防止文件句柄泄漏，确保所有未完成的写入操作被刷新到磁盘
     * @throws IOException 文件关闭失败时抛出
     */
    public void close() throws IOException {
        dbFile.close();
    }

    /**
     * 主方法：DiskManager的功能测试用例
     * 测试逻辑：循环20次 → 打开数据库文件 → 分配新页面 → 写入测试数据 → 读取验证 → 关闭文件
     * 用于验证页的分配、写入、读取功能是否正常，数据一致性是否达标
     */
    public static void main(String[] args) {
        // 循环20次，测试多页面的分配和读写
        for(int i=0;i<20;i++)
        {
            try {
                // 1. 初始化磁盘管理器，使用测试文件名"test_database.db"
                DiskManager diskManager = new DiskManager("test_database.db");

                // 2. 分配一个新页面，获取新页面ID
                int newPageId = diskManager.allocatePage();
                System.out.println("分配了一个新页面，ID = " + newPageId);

                // 3. 准备测试数据：创建PAGE_SIZE大小的字节数组，写入自定义字符串
                byte[] dataToWrite = new byte[DiskManager.PAGE_SIZE];
                String message = "Hello, Database World! This is page " + newPageId;
                byte[] messageBytes = message.getBytes();
                // 将字符串字节拷贝到页面数组的起始位置（剩余空间自动填充0）
                System.arraycopy(messageBytes, 0, dataToWrite, 0, messageBytes.length);

                // 4. 将测试数据写入新页面
                System.out.println("正在写入数据到页面 " + newPageId);
                diskManager.writePage(newPageId, dataToWrite);

                // 5. 准备空数组，用于读取刚刚写入的页面数据
                byte[] dataToRead = new byte[DiskManager.PAGE_SIZE];

                // 6. 从磁盘读取新页面数据
                System.out.println("正在从页面 " + newPageId + " 读取数据");
                diskManager.readPage(newPageId, dataToRead);

                // 7. 验证数据一致性：将读取的字节数组转为字符串，与原消息对比
                String readMessage = new String(dataToRead).trim(); // trim()去除末尾的空字节
                System.out.println("读取到的消息: " + readMessage);

                // 8. 打印验证结果
                if (message.equals(readMessage)) {
                    System.out.println("成功！写入和读取的数据一致！");
                } else {
                    System.out.println("失败！数据不一致！");
                }

                // 9. 关闭磁盘管理器，释放文件资源（避免每次循环泄漏句柄）
                diskManager.close();

            } catch (IOException e) {
                // 捕获并打印IO异常，便于调试
                e.printStackTrace();
            }
        }
    }
}